Skip to content

fix(driver-sql): Field.date is a tz-naive YYYY-MM-DD calendar day (ADR-0053 Phase 1)#1968

Merged
os-zhuang merged 1 commit into
mainfrom
fix/adr-0053-date-only-storage
Jun 16, 2026
Merged

fix(driver-sql): Field.date is a tz-naive YYYY-MM-DD calendar day (ADR-0053 Phase 1)#1968
os-zhuang merged 1 commit into
mainfrom
fix/adr-0053-date-only-storage

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Implements Phase 1 of ADR-0053: make Field.date a timezone-naive YYYY-MM-DD calendar day end-to-end at the SQL-driver boundary. Field.datetime is untouched (keeps full-instant UTC-ms semantics).

Refs ADR-0053, #1928.

Why — the silent date-equality miss

A Field.date ("close date", "due date", "birthday") is semantically a calendar day, but the driver treated it as an instant. The three layers disagreed:

Layer Old behaviour
Write (formatInput) stored the value verbatim — a Date/ISO kept its time (dev.db held close_date = "2026-07-15T17:24:56.533Z")
Filter (coerceFilterValue) already normalized the comparand to date-only YYYY-MM-DD
Read (formatOutput) no normalization — returned the stored timestamp

So close_date == '2026-07-15' compared "2026-07-15T17:24Z" against "2026-07-15"never equal. The same broke $in of dates and daysFromNow(n)-style equality comparands. Range filters ($gte/$lt) only worked by accident of lexicographic ISO ordering.

What changed (packages/plugins/driver-sql/src/sql-driver.ts)

  • Shared helper toDateOnly() — single source of truth for date-only truncation (Date → UTC calendar day; string → leading YYYY-MM-DD). coerceFilterValue now delegates to it, so filter/write/read all agree.
  • Write (formatInput): every Field.date value collapses to YYYY-MM-DD before insert/update, matching the filter's existing contract. Dialect-agnostic.
  • Read (formatOutput): Field.date returned as YYYY-MM-DD, slicing any stored time — transparently repairs legacy timestamped rows on read, no data migration. Read normalization now runs on the find path for every dialect too (previously only findOne); the json/boolean deserialisation stays SQLite-gated internally.

Out of scope (ADR-0053 Phase 2, needs separate review)

Timezone-aware today()/daysFromNow()/daysAgo(), org/user reference timezone, datetime render-time TZ. This PR is the low-risk root fix; it composes with the #1950 build lint (flow-date-equality-filter), which becomes a belt-and-suspenders hint.

Testing

  • New suite sql-driver-date-only.test.ts (8 tests): Date/ISO/date-only write→read round-trips collapse to YYYY-MM-DD; Field.datetime keeps its instant; the silent-miss regression (date-only equality matches a timestamped write); $in of days; range filters; and legacy-row read-repair + that an in-place rewrite makes a legacy row filter-matchable.
  • Full driver-sql suite green: 160 tests (152 existing + 8 new).
  • pnpm --filter @objectstack/example-crm build ✓ (ADR-0032 expression validation passes, so the days_to_close formula still compiles).

⚠️ Note for pg/mysql real DATE columns: a Date returned by the driver collapses on its UTC calendar day (consistent with the pre-existing coerceFilterValue contract). SQLite (TEXT affinity) is fully correct; a native-type parser for pg/mysql is left for a follow-up if needed.

🤖 Generated with Claude Code

…endar day (ADR-0053 Phase 1)

A `Field.date` is semantically a timezone-naive calendar day, but the SQL
driver treated it as an instant: `formatInput` wrote the value verbatim
(keeping its time component) while `coerceFilterValue` already normalized
filter comparands to date-only `YYYY-MM-DD`. That write/filter asymmetry
made date-equality filters (`close_date == '2026-07-15'`, `$in`,
`daysFromNow(n)`-style comparands) silently match nothing.

Align the write/read boundary with the filter's existing date-only contract:

- Extract the date-only truncation into a shared `toDateOnly` helper used by
  the filter, write, and read paths so all three agree on what a date is.
- `formatInput`: collapse every `Field.date` value (Date | full-ISO string |
  date-only string) to `YYYY-MM-DD` before insert/update. A Date collapses to
  its UTC calendar day, matching `coerceFilterValue`.
- `formatOutput`: return `Field.date` as `YYYY-MM-DD`, slicing any stored time
  component — transparently repairing legacy timestamped rows on read with no
  data migration. Read normalization now runs on the `find` path for every
  dialect (previously only `findOne`); the json/boolean deserialisation stays
  SQLite-gated internally.

`Field.datetime` is unchanged (full-instant UTC-ms semantics).

Out of scope (ADR-0053 Phase 2): tz-aware today()/daysFromNow()/daysAgo(),
org/user reference timezone, datetime render-time TZ.

Refs ADR-0053, #1928

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 16, 2026 3:33pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/core/plugins.mdx (via @objectstack/driver-sql)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/driver-sql)
  • content/docs/concepts/terminology.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/guides/driver-configuration.mdx (via @objectstack/driver-sql)
  • content/docs/guides/packages.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/driver-sql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant